/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jpxx.workflow;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.jpxx.workflow.util.WorkFlowGraphSerializer;

public class DirectedGraph implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 8014581137419733282L;

	private Node rootNode;
	private int graphId;
	private String name;
	private String description;
	private int version;

	private Map<String, Node> startNode = new HashMap<String, Node>();
	private Map<String, Node> endNode = new HashMap<String, Node>();

	/**
	 * Constructor
	 */
	public DirectedGraph() {
	}

	/**
	 * Creates a new instance of DirectedGraph with name
	 * 
	 * @param name
	 *            The name of the graph
	 */
	public DirectedGraph(String name) {
		this.name = name;
		this.description = name;
		this.version = -1;
	}

	/**
	 * Creates a new instance of DirectedGraph with name and version
	 * 
	 * @param name
	 *            The name of the graph
	 * @param version
	 *            The version of the graph
	 */
	public DirectedGraph(String name, int vers) {
		this.name = name;
		this.description = name;
		this.version = vers;
	}

	// Accessor methods

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return this.description;
	}

	public void setDescription(String d) {
		this.description = d;
	}

	public int getVersion() {
		return this.version;
	}

	public void setVersion(int version) {
		this.version = version;
	}

	public int getGraphId() {
		return graphId;
	}

	public void setGraphId(int i) {
		this.graphId = i;
	}

	/**
	 * Sets the root node of this graph.
	 * 
	 * @param Node
	 *            , the root node
	 */
	public void setRootNode(Node node) {
		this.rootNode = node;
	}

	/**
	 * Gets the root node of this graph
	 * 
	 * @returns Node, the root node
	 * 
	 */
	public Node getRootNode() {
		return this.rootNode;
	}

	/**
	 * Gets a node given the node Id
	 * 
	 * @returns Node
	 * 
	 */
	public Node getNode(int nodeId) {
		return rootNode.getNode(nodeId);
	}

	/**
	 * Validate a DirectedGraph
	 * 
	 * @throws WorkFlowException
	 */
	public void validate() throws WorkFlowException {
		validate(rootNode);
	}

	/**
	 * Helper function of validate()
	 * 
	 * @param node
	 * @throws WorkFlowException
	 */
	private void validate(Node node) throws WorkFlowException {

		if (node.isValidated()) {
			return;
		} else {
			node.setValidated();
		}

		String type = node.getNodeType();
		if (type.equals(NodeType.START.getType())) {
			validateStart(node);
		} else if (type.equals(NodeType.END.getType())) {
			validateEnd(node);
		} else if (type.equals(NodeType.AND.getType())) {
			validateAND(node);
		} else if (type.equals(NodeType.PROCESS.getType())) {
			validateProcess(node);
		} else if (type.equals(NodeType.CONTAINER.getType())) {
			validateContainer(node);
		} else if (type.equals(NodeType.OR.getType())) {
			validateOR(node);
		}
		if (startNode.size() == 0) { // rule#1
			throw new WorkFlowException("there is no Start node in the graph");
		}
		if (endNode.size() == 0) { // rule #3
			throw new WorkFlowException("there is no End node in the graph");
		}
	}

	private void validateStart(Node node) throws WorkFlowException {
		startNode.put(node.getNodeName(), node);
		if (startNode.size() != 1) { // rule #1
			throw new WorkFlowException("More than one Start node in the graph");
		} else { // only one start node
			if (node.getFromNodes().size() != 0) { // rule #2
				throw new WorkFlowException(
						"No nodes should go into Start node");
			} else {
				Vector<Destination> destinations = node.getDestinations();
				if (destinations.size() == 0) { // rule #2
					throw new WorkFlowException(
							"Start node should has at lease one node out");
				} else {
					for (int i = 0; i < destinations.size(); i++) {
						Destination d = (Destination) destinations.elementAt(i);
						String ntype = d.getNode().getNodeType();
						if (ntype.equals(NodeType.CONTAINER.getType())
								|| ntype.equals(NodeType.PROCESS.getType())) {
							validate(d.getNode());
						} else { // rule #2
							throw new WorkFlowException(
									"Start node should go into Container"
											+ " or Process node.");
						}
					}
				}
			}
		}

	}

	private void validateEnd(Node node) throws WorkFlowException {
		endNode.put(node.getNodeName(), node);
		if (endNode.size() != 1) { // rule #3
			throw new WorkFlowException("More than one End node in the graph");
		} else {
			if (node.getDestinations().size() != 0) { // rule #4
				throw new WorkFlowException(
						"No nodes should go out from End node");
			}
		}

	}

	private void validateAND(Node node) throws WorkFlowException {
		if (node.getFromNodes().size() < 2) { // rule #5
			throw new WorkFlowException(
					"AND node should have at least 2 nodes in");
		} else {
			Vector<Destination> destinations = node.getDestinations();
			if (destinations.size() == 0) { // rule #6
				throw new WorkFlowException(
						"AND node should has at lease one node out");
			} else {
				for (int i = 0; i < destinations.size(); i++) {
					Destination d = (Destination) destinations.elementAt(i);
					String ntype = d.getNode().getNodeType();
					if (ntype.equals(NodeType.CONTAINER.getType())
							|| ntype.equals(NodeType.PROCESS.getType())
							|| ntype.equals(NodeType.END.getType())
							|| ntype.equals(NodeType.AND.getType())
							|| ntype.equals(NodeType.OR.getType())) {
						validate(d.getNode());
					} else { // rule #7
						throw new WorkFlowException(
								"AND node should go into a Container,"
										+ " a Process, an AND, an OR or an End node.");
					}
				}
			}
		}
	}

	private void validateOR(Node node) throws WorkFlowException {
		if (node.getFromNodes().size() < 2) { // rule #5
			throw new WorkFlowException(
					"OR node should have at least 2 nodes in");
		} else {
			Vector<Destination> destinations = node.getDestinations();
			if (destinations.size() == 0) { // rule #6
				throw new WorkFlowException(
						"OR node should has at lease one node out");
			} else {
				for (int i = 0; i < destinations.size(); i++) {
					Destination d = (Destination) destinations.elementAt(i);
					String ntype = d.getNode().getNodeType();
					if (ntype.equals(NodeType.CONTAINER.getType())
							|| ntype.equals(NodeType.PROCESS.getType())
							|| ntype.equals(NodeType.END.getType())
							|| ntype.equals(NodeType.AND.getType())
							|| ntype.equals(NodeType.OR.getType())) {
						validate(d.getNode());
					} else { // rule #7
						throw new WorkFlowException(
								"OR node should go into a Container,"
										+ " a Process, an AND, an OR or an End node.");
					}
				}
			}
		}
	}

	private void validateProcess(Node node) throws WorkFlowException {
		Vector<Destination> destinations = node.getDestinations();
		if (destinations.size() == 0) { // rule #8
			throw new WorkFlowException(
					"Process node should has at lease one node out");
		} else {
			for (int i = 0; i < destinations.size(); i++) {
				Destination d = (Destination) destinations.elementAt(i);
				String ntype = d.getNode().getNodeType();
				if (ntype.equals(NodeType.CONTAINER.getType())
						|| ntype.equals(NodeType.PROCESS.getType())
						|| ntype.equals(NodeType.END.getType())
						|| ntype.equals(NodeType.AND.getType())
						|| ntype.equals(NodeType.OR.getType())) {
					validate(d.getNode());
				} else { // rule #9
					throw new WorkFlowException(
							"Process node should go into a Container,"
									+ " a Process, an AND, an OR or an End node.");
				}
			}
		}
	}

	private void validateContainer(Node node) throws WorkFlowException {
		Vector<Destination> destinations = node.getDestinations();
		for (int i = 0; i < destinations.size(); i++) {
			Destination d = (Destination) destinations.elementAt(i);
			String ntype = d.getNode().getNodeType();
			if (ntype.equals(NodeType.CONTAINER.getType())
					|| ntype.equals(NodeType.PROCESS.getType())
					|| ntype.equals(NodeType.END.getType())
					|| ntype.equals(NodeType.AND.getType())
					|| ntype.equals(NodeType.OR.getType())) {
				validate(d.getNode());
			} else { // rule #11
				throw new WorkFlowException(
						"Container node should go into a Container,"
								+ " a Process, an AND, an OR or End node.");
			}
		}
	}

	public String toXML() throws WorkFlowException {
		return WorkFlowGraphSerializer.serialize(this);
	}

	/**
	 * Gets a node given the node name
	 * 
	 * @returns Node
	 * 
	 */
	public Node getNode(String nodeName) {
		return rootNode.getNode(nodeName);
	}

	public Node getEndNode() {
		return rootNode.getNode(NodeType.END.getType());
		// NB. End nodes always have the name "End"
	}

	public Vector<Node> getNodes(String nodeType) {
		return rootNode.getNodes(nodeType);
	}

	public Vector<Node> getAllNodes() {
		return rootNode.getNodes();
	}
}